from datetime import datetime
from time import time
from app import app, db, login
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from hashlib import md5
import jwt



# 粉丝机制
# followers关联表
followers = db.Table('followers',
    db.Column('follower_id', db.Integer, db.ForeignKey('user.id')),
    db.Column('followed_id', db.Integer, db.ForeignKey('user.id'))
)

# User类继承自db.Model，它是Flask-SQLAlchemy中所有模型的基类
# user是数据库表的名称，Flask-SQLAlchemy自动设置类名为小写来作为对应表的名称

# Flask-Login插件需要在用户模型上实现某些属性和方法
# 只要将这些必需项添加到模型中，Flask-Login就没有其他依赖了，
# 它就可以与基于任何数据库系统的用户模型一起工作
# # 必须的四项如下：
    # is_authenticated: 一个用来表示用户是否通过登录认证的属性，用True和False表示。
    # is_active: 如果用户账户是活跃的，那么这个属性是True，否则就是False（译者注：活跃用户的定义是该用户的登录状态是否通过用户名密码登录，通过“记住我”功能保持登录状态的用户是非活跃的）。
    # is_anonymous: 常规用户的该属性是False，对特定的匿名用户是True。
    # get_id(): 返回用户的唯一id的方法，返回值类型是字符串(Python 2下返回unicode字符串).
# 很容易地实现这四个属性或方法，但是由于它们是相当通用的，
# 因此Flask-Login提供了一个叫做UserMixin的mixin类来将它们归纳其中
class User(UserMixin, db.Model):
    # 表的字段定义为类属性，字段被创建为db.Column类的实例，它传入字段类型以及其他可选参数
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)
    email = db.Column(db.String(120), index=True, unique=True)
    password_hash = db.Column(db.String(128))
    about_me = db.Column(db.String(140))
    last_seen = db.Column(db.DateTime, default=datetime.utcnow)
    # User类有一个新的posts字段，用db.relationship初始化
    # 这不是实际的数据库字段，而是用户和其动态之间关系的高级视图，因此它不在数据库图表中
    # 对于一对多关系，db.relationship字段通常在“一”的这边定义，并用作访问“多”的便捷方式
    # 如果我有一个用户实例u，表达式u.posts将运行一个数据库查询，返回该用户发表过的所有动态
    
    # db.relationship的第一个参数表示代表关系“多”的类。 
    # backref参数定义了代表“多”的类的实例反向调用“一”的时候的属性名称。
    # 这将会为用户动态添加一个属性post.author，调用它将返回给该用户动态的用户实例
    # lazy参数定义了这种关系调用的数据库查询是如何执行的

    # 在User类中创建的db.relationship为用户添加了posts属性，
    # 并为用户动态添加了author属性。 
    # 我使用author虚拟字段来调用其作者，而不必通过用户ID来处理。
    posts = db.relationship('Post', backref='author', lazy='dynamic')


    # db.relationship函数来定义模型类中的关系, 将User实例关联到其他User实例
    # 对于通过这种关系关联的一对用户来说，左侧用户关注右侧用户
        # 我在左侧的用户中定义了followed的关系，
        # 因为当我从左侧查询这个关系时，我将得到已关注的用户列表（即右侧的列表）
    # db.relationship() 参数
        # 'User'是关系当中的右侧实体（将左侧实体看成是上级类）。由于这是自引用关系，所以我不得不在两侧都使用同一个实体
        # secondary 指定了用于该关系的关联表，就是使用我在上面定义的followers。
        #  primaryjoin 指明了通过关系表关联到左侧实体（关注者）的条件 。
            # 关系中的左侧的join条件是关系表中的follower_id字段与这个关注者的用户ID匹配。
            # followers.c.follower_id表达式引用了该关系表中的follower_id列。
        # secondaryjoin 指明了通过关系表关联到右侧实体（被关注者）的条件 。 
            # 这个条件与primaryjoin类似，
            # 唯一的区别在于，现在我使用关系表的字段的是followed_id了。
        # backref定义了右侧实体如何访问该关系。
            # 在左侧，关系被命名为followed，所以在右侧我将使用followers来表示所有左侧用户的列表，即粉丝列表。
            # 附加的lazy参数表示这个查询的执行模式，设置为动态模式的查询不会立即执行，直到被调用，这也是我设置用户动态一对多的关系的方式。
        # lazy和backref中的lazy类似，只不过当前的这个是应用于左侧实体，backref中的是应用于右侧实体。
    followed = db.relationship(
        'User',
        secondary=followers,
        primaryjoin=(followers.c.follower_id == id),
        secondaryjoin=(followers.c.followed_id == id),
        backref=db.backref('followers', lazy='dynamic'),
        lazy='dynamic'
    )

    # 类的__repr__方法用于在调试时打印用户实例
    def __repr__(self):
        return '<User {}>'.format(self.username)

    #  password
    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

    # User类新增的avatar()方法需要传入需求头像的像素大小，并返回用户头像图片的URL
    def avatar(self, size):
        # Python中的MD5的参数类型需要是字节而不是字符串
        digest = md5(self.email.lower().encode('utf-8')).hexdigest()
        # <hash>是用户的电子邮件地址的MD5哈希值
        # s参数来请求不同大小的图片, 128x128像素的头像, ?s=128
        # 参数d让Gravatar为没有向服务注册头像的用户提供的随机头像, 机头像类型是“identicon”
        # 它为每个邮箱都返回一个漂亮且不重复的几何设计图片
        # 一些Web浏览器插件（如Ghostery）会屏蔽Gravatar图像
        return 'https://www.gravatar.com/avatar/{}?d=identicon&s={}'.format(
            digest, size)
        
    # Follow and unfollow
    def follow(self, user):
        if not self.is_following(user):
            self.followed.append(user)

    def unfollow(self, user):
        if self.is_following(user):
            self.followed.remove(user)
    # filter()方法与filter_by()很类似，但是更加偏向底层，因为它可以包含任意的过滤条件，
    # 而不像filter_by()，它只能检查是否等于一个常量值

    # 在is_following()中使用的过滤条件是，查找关联表中左侧外键设置为self用户且右侧设置为user参数的数据行。 
    # 查询以count()方法结束，返回结果的数量。 
    # 这个查询的结果是0或1，因此检查计数是1还是大于0实际上是相等的
    def is_following(self, user):
        return self.followed.filter(
            followers.c.followed_id == user.id).count() > 0


    # 查看自身动态和关注的用户动态
    def followed_posts(self):
        # 查看已关注用户的动态
        # Post.query.join(...).filter(...).order_by(...)
        followed = Post.query.join(
            followers, (followers.c.followed_id == Post.user_id)).filter(
                followers.c.follower_id == self.id)
        # 查看自身动态
        own = Post.query.filter_by(user_id=self.id)
        # 组合自身动态和关注的用户动态
        return followed.union(own).order_by(Post.timestamp.desc())

    # 密码重置 by email
    # 以字符串形式生成一个JWT令牌
    # decode('utf-8')是必须的，
    #因为jwt.encode()函数将令牌作为字节序列返回，
    # 但是在应用中将令牌表示为字符串更方便。
    def get_reset_password_token(self, expires_in=600):
        return jwt.encode(
            {'reset_password': self.id, 'exp': time() + expires_in},
            app.config['SECRET_KEY'], algorithm='HS256').decode('utf-8')

    # 静态方法，这意味着它可以直接从类中调用
    # 静态方法与类方法类似，唯一的区别是静态方法不会接收类作为第一个参数
    # 这个方法需要一个令牌，并尝试通过调用PyJWT的jwt.decode()函数来解码它
    # 如果令牌不能被验证或已过期，将会引发异常
    @staticmethod
    def verify_reset_password_token(token):
        try:
            id = jwt.decode(token, app.config['SECRET_KEY'],
                            algorithms=['HS256'])['reset_password']
        except:
            return None
        return User.query.get(id)

# 数据库对Flask-Login透明，所以需要应用来辅助加载用户
# 插件期望应用配置一个用户加载函数，可以调用该函数来加载给定ID的用户
# Flask-Login的@login.user_loader装饰器来为用户加载功能注册函数
@login.user_loader
def load_user(id):
    return User.query.get(int(id))


class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.String(140))
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))

    def __repr__(self):
        return '<Post {}>'.format(self.body)

